/*
 * Copyright 2010-2025 Hyland Software, Inc. and its affiliates.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.activiti.engine.test.cfg.executioncount;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.List;
import java.util.Map;
import org.activiti.engine.history.HistoricActivityInstance;
import org.activiti.engine.impl.cfg.CommandExecutorImpl;
import org.activiti.engine.impl.db.DbSqlSessionFactory;
import org.activiti.engine.impl.history.DefaultHistoryManager;
import org.activiti.engine.impl.history.HistoryLevel;
import org.activiti.engine.impl.interceptor.CommandInterceptor;
import org.activiti.engine.impl.test.PluggableActivitiTestCase;
import org.activiti.engine.repository.Deployment;
import org.activiti.engine.task.Task;
import org.activiti.engine.test.profiler.ActivitiProfiler;
import org.activiti.engine.test.profiler.CommandStats;
import org.activiti.engine.test.profiler.ConsoleLogger;
import org.activiti.engine.test.profiler.ProfileSession;
import org.activiti.engine.test.profiler.ProfilingDbSqlSessionFactory;
import org.activiti.engine.test.profiler.TotalExecutionTimeCommandInterceptor;

/**

 */
public class VerifyDatabaseOperationsTest extends PluggableActivitiTestCase {

    protected boolean oldExecutionTreeFetchValue;
    protected boolean oldExecutionRelationshipCountValue;
    protected boolean oldenableProcessDefinitionInfoCacheValue;
    protected CommandInterceptor oldFirstCommandInterceptor;
    protected DbSqlSessionFactory oldDbSqlSessionFactory;
    protected HistoryLevel oldHistoryLevel;

    @Override
    protected void setUp() throws Exception {
        super.setUp();

        // Enable flags
        this.oldExecutionTreeFetchValue = processEngineConfiguration
            .getPerformanceSettings()
            .isEnableEagerExecutionTreeFetching();
        this.oldExecutionRelationshipCountValue = processEngineConfiguration
            .getPerformanceSettings()
            .isEnableExecutionRelationshipCounts();
        this.oldenableProcessDefinitionInfoCacheValue = processEngineConfiguration.isEnableProcessDefinitionInfoCache();
        oldHistoryLevel = ((DefaultHistoryManager) processEngineConfiguration.getHistoryManager()).getHistoryLevel();

        processEngineConfiguration.getPerformanceSettings().setEnableEagerExecutionTreeFetching(true);
        processEngineConfiguration.getPerformanceSettings().setEnableExecutionRelationshipCounts(true);
        processEngineConfiguration.setEnableProcessDefinitionInfoCache(false);
        ((DefaultHistoryManager) processEngineConfiguration.getHistoryManager()).setHistoryLevel(HistoryLevel.AUDIT);

        // The time interceptor should be first
        CommandExecutorImpl commandExecutor = ((CommandExecutorImpl) processEngineConfiguration.getCommandExecutor());
        this.oldFirstCommandInterceptor = commandExecutor.getFirst();

        TotalExecutionTimeCommandInterceptor timeCommandInterceptor = new TotalExecutionTimeCommandInterceptor();
        timeCommandInterceptor.setNext(oldFirstCommandInterceptor);
        commandExecutor.setFirst(timeCommandInterceptor);

        // Add dbsqlSession factory that captures CRUD operations
        this.oldDbSqlSessionFactory = processEngineConfiguration.getDbSqlSessionFactory();
        DbSqlSessionFactory newDbSqlSessionFactory = new ProfilingDbSqlSessionFactory();
        newDbSqlSessionFactory.setDatabaseType(oldDbSqlSessionFactory.getDatabaseType());
        newDbSqlSessionFactory.setDatabaseTablePrefix(oldDbSqlSessionFactory.getDatabaseTablePrefix());
        newDbSqlSessionFactory.setTablePrefixIsSchema(oldDbSqlSessionFactory.isTablePrefixIsSchema());
        newDbSqlSessionFactory.setDatabaseCatalog(oldDbSqlSessionFactory.getDatabaseCatalog());
        newDbSqlSessionFactory.setDatabaseSchema(oldDbSqlSessionFactory.getDatabaseSchema());
        newDbSqlSessionFactory.setSqlSessionFactory(oldDbSqlSessionFactory.getSqlSessionFactory());
        newDbSqlSessionFactory.setIdGenerator(oldDbSqlSessionFactory.getIdGenerator());
        newDbSqlSessionFactory.setDbHistoryUsed(oldDbSqlSessionFactory.isDbHistoryUsed());
        processEngineConfiguration.addSessionFactory(newDbSqlSessionFactory);
    }

    @Override
    protected void tearDown() throws Exception {
        processEngineConfiguration
            .getPerformanceSettings()
            .setEnableEagerExecutionTreeFetching(oldExecutionTreeFetchValue);
        processEngineConfiguration
            .getPerformanceSettings()
            .setEnableExecutionRelationshipCounts(oldExecutionRelationshipCountValue);
        processEngineConfiguration.setEnableProcessDefinitionInfoCache(oldenableProcessDefinitionInfoCacheValue);
        ((DefaultHistoryManager) processEngineConfiguration.getHistoryManager()).setHistoryLevel(oldHistoryLevel);

        ((CommandExecutorImpl) processEngineConfiguration.getCommandExecutor()).setFirst(oldFirstCommandInterceptor);

        processEngineConfiguration.addSessionFactory(oldDbSqlSessionFactory);

        // Validate (cause this tended to be screwed up)
        List<HistoricActivityInstance> historicActivityInstances = historyService
            .createHistoricActivityInstanceQuery()
            .list();
        for (HistoricActivityInstance historicActivityInstance : historicActivityInstances) {
            assertThat(historicActivityInstance.getStartTime() != null).isTrue();
            assertThat(historicActivityInstance.getEndTime() != null).isTrue();
        }

        ActivitiProfiler.getInstance().reset();

        for (Deployment deployment : repositoryService.createDeploymentQuery().list()) {
            repositoryService.deleteDeployment(deployment.getId(), true);
        }
        super.tearDown();
    }

    public void testStartToEnd() {
        deployStartProcessInstanceAndProfile("process01.bpmn20.xml", "process01");

        assertDatabaseSelects("StartProcessInstanceCmd", "selectLatestProcessDefinitionByKey", 1L, "selectIdentityLinksByProcessInstance", 1L);

        assertDatabaseInserts(
            "StartProcessInstanceCmd",
            "HistoricActivityInstanceEntityImpl-bulk-with-2",
            1L,
            "HistoricProcessInstanceEntityImpl",
            1L
        );

        assertNoUpdatesAndDeletes("StartProcessInstanceCmd");

        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);
        assertThat(historyService.createHistoricProcessInstanceQuery().finished().count()).isEqualTo(1);
    }

    public void testVariablesAndPassthrough() {
        deployStartProcessInstanceAndProfile(
            "process-variables-servicetask01.bpmn20.xml",
            "process-variables-servicetask01"
        );

        assertDatabaseSelects("StartProcessInstanceCmd", "selectLatestProcessDefinitionByKey", 1L, "selectIdentityLinksByProcessInstance", 1L);
        assertDatabaseInserts(
            "StartProcessInstanceCmd",
            "HistoricVariableInstanceEntityImpl-bulk-with-4",
            1L,
            "HistoricProcessInstanceEntityImpl",
            1L,
            "HistoricActivityInstanceEntityImpl-bulk-with-9",
            1L
        );
        assertNoUpdatesAndDeletes("StartProcessInstanceCmd");

        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);
        assertThat(historyService.createHistoricProcessInstanceQuery().finished().count()).isEqualTo(1);
    }

    public void testManyVariablesViaServiceTaskAndPassthroughs() {
        deployStartProcessInstanceAndProfile(
            "process-variables-servicetask02.bpmn20.xml",
            "process-variables-servicetask02"
        );

        assertDatabaseSelects("StartProcessInstanceCmd", "selectLatestProcessDefinitionByKey", 1L, "selectIdentityLinksByProcessInstance", 1L);
        assertDatabaseInserts(
            "StartProcessInstanceCmd",
            "HistoricVariableInstanceEntityImpl-bulk-with-50",
            1L,
            "HistoricProcessInstanceEntityImpl",
            1L,
            "HistoricActivityInstanceEntityImpl-bulk-with-9",
            1L
        );
        assertNoUpdatesAndDeletes("StartProcessInstanceCmd");

        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);
        assertThat(historyService.createHistoricProcessInstanceQuery().finished().count()).isEqualTo(1);
    }

    public void testOnlyPassThroughs() {
        deployStartProcessInstanceAndProfile("process02.bpmn20.xml", "process02");

        assertDatabaseSelects("StartProcessInstanceCmd", "selectLatestProcessDefinitionByKey", 1L, "selectIdentityLinksByProcessInstance", 1L);
        assertDatabaseInserts(
            "StartProcessInstanceCmd",
            "HistoricActivityInstanceEntityImpl-bulk-with-9",
            1L,
            "HistoricProcessInstanceEntityImpl",
            1L
        );
        assertNoUpdatesAndDeletes("StartProcessInstanceCmd");

        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);
        assertThat(historyService.createHistoricProcessInstanceQuery().finished().count()).isEqualTo(1);
    }

    public void testParallelForkAndJoin() {
        deployStartProcessInstanceAndProfile("process03.bpmn20.xml", "process03");

        assertDatabaseSelects("StartProcessInstanceCmd", "selectLatestProcessDefinitionByKey", 1L, "selectIdentityLinksByProcessInstance", 1L);
        assertDatabaseInserts(
            "StartProcessInstanceCmd",
            "HistoricActivityInstanceEntityImpl-bulk-with-7",
            1L,
            "HistoricProcessInstanceEntityImpl",
            1L
        );
        assertNoUpdatesAndDeletes("StartProcessInstanceCmd");

        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);
        assertThat(historyService.createHistoricProcessInstanceQuery().finished().count()).isEqualTo(1);
    }

    public void testNestedParallelForkAndJoin() {
        deployStartProcessInstanceAndProfile("process04.bpmn20.xml", "process04");

        assertDatabaseSelects("StartProcessInstanceCmd", "selectLatestProcessDefinitionByKey", 1L, "selectIdentityLinksByProcessInstance", 1L);
        assertDatabaseInserts(
            "StartProcessInstanceCmd",
            "HistoricActivityInstanceEntityImpl-bulk-with-21",
            1L,
            "HistoricProcessInstanceEntityImpl",
            1L
        );
        assertNoUpdatesAndDeletes("StartProcessInstanceCmd");

        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);
        assertThat(historyService.createHistoricProcessInstanceQuery().finished().count()).isEqualTo(1);
    }

    public void testExlusiveGateway() {
        deployStartProcessInstanceAndProfile("process05.bpmn20.xml", "process05");

        assertDatabaseSelects("StartProcessInstanceCmd", "selectLatestProcessDefinitionByKey", 1L, "selectIdentityLinksByProcessInstance", 1L);
        assertDatabaseInserts(
            "StartProcessInstanceCmd",
            "HistoricActivityInstanceEntityImpl-bulk-with-5",
            1L,
            "HistoricProcessInstanceEntityImpl",
            1L,
            "HistoricVariableInstanceEntityImpl",
            1L
        );
        assertNoUpdatesAndDeletes("StartProcessInstanceCmd");

        assertThat(runtimeService.createProcessInstanceQuery().count()).isEqualTo(0);
        assertThat(historyService.createHistoricProcessInstanceQuery().finished().count()).isEqualTo(1);
    }

    public void testOneTaskProcess() {
        deployStartProcessInstanceAndProfile("process-usertask-01.bpmn20.xml", "process-usertask-01", false);
        Task task = taskService.createTaskQuery().singleResult();
        taskService.complete(task.getId());
        stopProfiling();

        assertExecutedCommands("StartProcessInstanceCmd", "org.activiti.engine.impl.TaskQueryImpl", "CompleteTaskCmd");

        // Start process instance
        assertDatabaseSelects("StartProcessInstanceCmd", "selectLatestProcessDefinitionByKey", 1L);
        assertDatabaseInserts(
            "StartProcessInstanceCmd",
            "ExecutionEntityImpl-bulk-with-2",
            1L,
            "TaskEntityImpl",
            1L,
            "HistoricActivityInstanceEntityImpl-bulk-with-2",
            1L,
            "HistoricTaskInstanceEntityImpl",
            1L,
            "HistoricProcessInstanceEntityImpl",
            1L
        );
        assertNoUpdatesAndDeletes("StartProcessInstanceCmd");

        // Task Query
        assertDatabaseSelects("org.activiti.engine.impl.TaskQueryImpl", "selectTaskByQueryCriteria", 1L);
        assertNoInserts("org.activiti.engine.impl.TaskQueryImpl");
        assertNoUpdates("org.activiti.engine.impl.TaskQueryImpl");
        assertNoDeletes("org.activiti.engine.impl.TaskQueryImpl");

        // Task Complete

        // TODO: implement counting for tasks similar to executions

        assertDatabaseSelects(
            "CompleteTaskCmd",
            "selectById org.activiti.engine.impl.persistence.entity.HistoricProcessInstanceEntityImpl",
            1L,
            "selectById org.activiti.engine.impl.persistence.entity.HistoricTaskInstanceEntityImpl",
            1L,
            "selectById org.activiti.engine.impl.persistence.entity.TaskEntityImpl",
            1L,
            "selectUnfinishedHistoricActivityInstanceExecutionIdAndActivityId",
            2L,
            "selectTasksByParentTaskId",
            1L,
            "selectIdentityLinksByTask",
            1L,
            "selectVariablesByTaskId",
            1L,
            "selectExecutionsWithSameRootProcessInstanceId",
            1L,
            "selectTasksByExecutionId",
            1L,
            "selectIdentityLinksByProcessInstance", 1L
        );
        assertDatabaseInserts("CompleteTaskCmd", "HistoricActivityInstanceEntityImpl", 1L);
        assertNoUpdates("CompleteTaskCmd");
        assertDatabaseDeletes("CompleteTaskCmd", "TaskEntityImpl", 1L, "ExecutionEntityImpl", 2L); // execution and processinstance
    }

    // ---------------------------------
    // HELPERS
    // ---------------------------------

    protected void assertExecutedCommands(String... commands) {
        ProfileSession profileSession = ActivitiProfiler.getInstance().getProfileSessions().get(0);
        Map<String, CommandStats> allStats = profileSession.calculateSummaryStatistics();

        if (commands.length != allStats.size()) {
            System.out.println("Following commands were found: ");
            for (String command : allStats.keySet()) {
                System.out.println(command);
            }
        }
        assertThat(allStats).hasSize(commands.length);

        for (String command : commands) {
            assertThat(getStatsForCommand(command, allStats)).as("Could not get stats for " + command).isNotNull();
        }
    }

    protected void assertDatabaseSelects(String commandClass, Object... expectedSelects) {
        CommandStats stats = getStats(commandClass);
        assertThat(stats.getDbSelects()).hasSize(expectedSelects.length / 2);

        for (int i = 0; i < expectedSelects.length; i += 2) {
            String dbSelect = (String) expectedSelects[i];
            Long count = (Long) expectedSelects[i + 1];

            assertThat(stats.getDbSelects().get(dbSelect)).as("Wrong select count for " + dbSelect).isEqualTo(count);
        }
    }

    protected void assertDatabaseInserts(String commandClass, Object... expectedInserts) {
        CommandStats stats = getStats(commandClass);

        if (expectedInserts.length / 2 != stats.getDbInserts().size()) {
            fail(
                "Unexpected number of database inserts : " +
                    stats.getDbInserts().size() +
                    ", but expected " +
                    expectedInserts.length / 2
            );
        }

        for (int i = 0; i < expectedInserts.length; i += 2) {
            String dbInsert = (String) expectedInserts[i];
            Long count = (Long) expectedInserts[i + 1];

            assertThat(stats.getDbInserts().get("org.activiti.engine.impl.persistence.entity." + dbInsert))
                .as("Insert count for " + dbInsert + "not correct")
                .isEqualTo(count);
        }
    }

    protected void assertDatabaseDeletes(String commandClass, Object... expectedDeletes) {
        CommandStats stats = getStats(commandClass);

        if (expectedDeletes.length / 2 != stats.getDbDeletes().size()) {
            fail("Unexpected number of database deletes : " + stats.getDbDeletes().size());
        }

        for (int i = 0; i < expectedDeletes.length; i += 2) {
            String dbDelete = (String) expectedDeletes[i];
            Long count = (Long) expectedDeletes[i + 1];

            assertThat(stats.getDbDeletes().get("org.activiti.engine.impl.persistence.entity." + dbDelete))
                .as("Delete count for " + dbDelete + "not correct")
                .isEqualTo(count);
        }
    }

    protected void assertNoInserts(String commandClass) {
        CommandStats stats = getStats(commandClass);
        assertThat(stats.getDbInserts()).hasSize(0);
    }

    protected void assertNoUpdatesAndDeletes(String commandClass) {
        assertNoDeletes(commandClass);
        assertNoUpdates(commandClass);
    }

    protected void assertNoDeletes(String commandClass) {
        CommandStats stats = getStats(commandClass);
        assertThat(stats.getDbDeletes()).hasSize(0);
    }

    protected void assertNoUpdates(String commandClass) {
        CommandStats stats = getStats(commandClass);
        assertThat(stats.getDbUpdates()).hasSize(0);
    }

    protected CommandStats getStats(String commandClass) {
        ProfileSession profileSession = ActivitiProfiler.getInstance().getProfileSessions().get(0);
        Map<String, CommandStats> allStats = profileSession.calculateSummaryStatistics();
        CommandStats stats = getStatsForCommand(commandClass, allStats);
        return stats;
    }

    protected CommandStats getStatsForCommand(String commandClass, Map<String, CommandStats> allStats) {
        String clazz = commandClass;
        if (!clazz.startsWith("org.activiti")) {
            clazz = "org.activiti.engine.impl.cmd." + clazz;
        }
        CommandStats stats = allStats.get(clazz);
        return stats;
    }

    // HELPERS

    protected ActivitiProfiler deployStartProcessInstanceAndProfile(String path, String processDefinitionKey) {
        return deployStartProcessInstanceAndProfile(path, processDefinitionKey, true);
    }

    protected ActivitiProfiler deployStartProcessInstanceAndProfile(
        String path,
        String processDefinitionKey,
        boolean stopProfilingAfterStart
    ) {
        deploy(path);
        ActivitiProfiler activitiProfiler = startProcessInstanceAndProfile(processDefinitionKey);
        if (stopProfilingAfterStart) {
            stopProfiling();
        }
        return activitiProfiler;
    }

    protected void deploy(String path) {
        repositoryService
            .createDeployment()
            .addClasspathResource("org/activiti/engine/test/cfg/executioncount/" + path)
            .deploy();
    }

    protected ActivitiProfiler startProcessInstanceAndProfile(String processDefinitionKey) {
        ActivitiProfiler activitiProfiler = ActivitiProfiler.getInstance();
        activitiProfiler.startProfileSession("Profiling session");
        runtimeService.startProcessInstanceByKey(processDefinitionKey);
        return activitiProfiler;
    }

    protected void stopProfiling() {
        ActivitiProfiler profiler = ActivitiProfiler.getInstance();
        profiler.stopCurrentProfileSession();
        new ConsoleLogger(profiler).log();
    }
}
